#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
using System.Windows.Forms;

using Osh;

namespace OshHost
{
	public class OshSource : IAdviseSink
	{
		private class NativeMethods
		{
			[DllImport("gdi32.dll", SetLastError=true)]
			public static extern IntPtr CreateCompatibleDC(IntPtr hdc);
			
			[DllImport("gdi32.dll")]
			public static extern bool DeleteDC(IntPtr hdc);

			[DllImport("gdi32.dll")]
			public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

			[DllImport("gdi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
			public static extern int GetObject(IntPtr hObject, int nSize, [In, Out] Win32.BITMAP bitmap);

			[DllImport("gdi32.dll")]
			public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);

			[DllImport("gdi32.dll")]
			public static extern int GetDIBits(IntPtr hdc, IntPtr hbmp, uint uStartScan,
				uint cScanLines, IntPtr lpvBits, ref Win32.BITMAPINFO_FLAT lpbmi, uint uUsage);

			[DllImport("gdi32.dll")]
			public static extern bool DeleteObject(IntPtr hObject);

			[DllImport("gdi32.dll")]
			public static extern uint SetPixel(IntPtr hdc, int X, int Y, uint crColor);

			[DllImport("user32.dll")]
			public static extern IntPtr GetDC(HandleRef hWnd);

			[DllImport("gdi32.dll")]
			public static extern bool OffsetViewportOrgEx(IntPtr hdc, int nXOffset, int nYOffset, [Out] out Win32.POINT lpPoint);

			[DllImport("gdi32.dll")]
			public static extern bool SetViewportOrgEx(IntPtr hdc, int x, int y, [Out] out Win32.POINT lpPoint);

		}

		private OshApplication _osh;
		private WebBrowserEx _webBrowser;
		private OSH.SourceClass _oshSource;
		private Graphics _screen = Graphics.FromHwnd(IntPtr.Zero);
		private IntPtr _offscreenBitmap;
		private Size _offscreenBitmapSize;
		private Size MaxBitmapSize = new Size(1024, 1024);
		private int BitmapSizeImageOffset = Marshal.OffsetOf(typeof(Win32.BITMAPINFOHEADER), "biSizeImage").ToInt32();

		public OshSource(OshApplication osh, WebBrowserEx webBrowser)
		{
			_osh = osh;
			_webBrowser = webBrowser;

			NativeInterfaces.IViewObject viewObject = _webBrowser.ActiveXInstance as NativeInterfaces.IViewObject;
			viewObject.SetAdvise((int)Win32.DVASPECT.DVASPECT_CONTENT, 0, this as IAdviseSink);
		}

		public bool Visible
		{
			get { return _oshSource != null && _oshSource.Visible; }
			set
			{ 
			    if(_oshSource == null)
			        return;
			    _oshSource.Visible = value;
			}
		}

		public WebBrowserEx WebBrowser
		{
			get { return _webBrowser; }
		}

		public void Refresh()
		{
			//Osh.Log.Write(Configuration.Local.OshTraceSource, "Osh.Refresh() start");
			CreateOffscreenWebBitmap();
			//Osh.Log.Write(Configuration.Local.OshTraceSource, "Osh.Refresh() stop");
		}

		private void CreateOffscreenWebBitmap()
		{
			EnsureOffscreenBitmap();

			IntPtr hdc = NativeMethods.CreateCompatibleDC(IntPtr.Zero);
			if(hdc == IntPtr.Zero)
				throw new ApplicationException("Cannot create graphics device context.");

			try
			{
				NativeMethods.SelectObject(hdc, _offscreenBitmap);

				// Adjust transparent color in accordance with the color capabilities of the context.
				uint transparentColor = NativeMethods.SetPixel(hdc, 0, 0, (uint)_osh.TransparentColor);
				
				CaptureWebBitmapScreen(hdc, _offscreenBitmap, _offscreenBitmapSize);

				SaveBitmap(hdc, _offscreenBitmap, _offscreenBitmapSize, transparentColor);
			}
			finally
			{
				NativeMethods.DeleteDC(hdc);
			}
		}

		private void DeleteOffscreenBitmap()
		{
			NativeMethods.DeleteObject(_offscreenBitmap);
			_offscreenBitmap = IntPtr.Zero;
		}

		private void EnsureOffscreenBitmap()
		{
			if(_offscreenBitmap == IntPtr.Zero ||
				_offscreenBitmapSize.Width < _osh.Width || _offscreenBitmapSize.Height < _osh.Height)
			{
				if(_offscreenBitmap != IntPtr.Zero)
					DeleteOffscreenBitmap();

				_offscreenBitmap = NativeMethods.CreateCompatibleBitmap(_screen.GetHdc(), _osh.Width, _osh.Height);
				if(_offscreenBitmap == IntPtr.Zero)
					throw new ApplicationException("Cannot create compatible bitmap.");

				Win32.BITMAP bitmap = new Win32.BITMAP();
				int result = NativeMethods.GetObject(_offscreenBitmap, Marshal.SizeOf(typeof(Win32.BITMAP)), bitmap);
				if(result == 0)
					throw new ApplicationException("Cannot get bitmap info.");

				_offscreenBitmapSize = new Size(bitmap.bmWidth, bitmap.bmHeight);
			}
		}

		private void CaptureWebBitmapScreen(IntPtr hdc, IntPtr offscreenBitmap, Size offscreenBitmapSize)
		{
			Win32.POINT lastOrigin;
			bool restoreOrigin = NativeMethods.SetViewportOrgEx(hdc, -_osh.Left, -_osh.Top, out lastOrigin);
			try
			{
				Win32.COMRECT rect = new Win32.COMRECT(0, 0, _webBrowser.Width, _webBrowser.Height);

				//Osh.Log.Write(Configuration.Local.OshTraceSource, "IViewObject.Draw() start");
				NativeInterfaces.IViewObject viewObject = _webBrowser.ActiveXInstance as NativeInterfaces.IViewObject;
				int hr = viewObject.Draw((uint)Win32.DVASPECT.DVASPECT_CONTENT, 0, IntPtr.Zero, null, IntPtr.Zero,
					hdc, rect, null, IntPtr.Zero, 0);
				//Osh.Log.Write(Configuration.Local.OshTraceSource, "IViewObject.Draw() stop");

				Marshal.ThrowExceptionForHR(hr);
			}
			finally
			{
				if(restoreOrigin)
					NativeMethods.SetViewportOrgEx(hdc, lastOrigin.x, lastOrigin.y, out lastOrigin);
			}
		}

		private void SaveBitmap(IntPtr hdc, IntPtr offscreenBitmap, Size offscreenBitmapSize, uint transparentColor)
		{
			if(_oshSource == null)
				throw new InvalidOperationException("No OSH source has been registered.");

			OSH.IBitmap bitmap = _oshSource.CreateBitmap(offscreenBitmapSize.Width, offscreenBitmapSize.Height);
			try
			{
				IntPtr bitmapInfoPtr = new IntPtr(bitmap.BitmapInfo);

				SaveBitmap(hdc, offscreenBitmap, offscreenBitmapSize, transparentColor, bitmapInfoPtr);

				_oshSource.ShowAndCloseBitmap(bitmap);
			}
			finally
			{
				Marshal.ReleaseComObject(bitmap);
			}
		}

		private unsafe void SaveBitmap(IntPtr hdc, IntPtr offscreenBitmap, Size offscreenBitmapSize, uint transparentColor,
			IntPtr bitmap)
		{
			int bitmapInfoHeaderSize = Marshal.SizeOf(typeof(Win32.BITMAPINFOHEADER));
			byte* bitmapHeader = (byte*)bitmap.ToPointer();
			byte* bitmapData = (byte*)bitmap.ToPointer() + bitmapInfoHeaderSize;

			Win32.BITMAPINFO_FLAT bitmapInfo = new Win32.BITMAPINFO_FLAT();
			bitmapInfo.bmiHeader_biSize = bitmapInfoHeaderSize;
			bitmapInfo.bmiHeader_biWidth = offscreenBitmapSize.Width;
			bitmapInfo.bmiHeader_biHeight = offscreenBitmapSize.Height;
			bitmapInfo.bmiHeader_biPlanes = 1;
			bitmapInfo.bmiHeader_biBitCount = 32;
			bitmapInfo.bmiHeader_biCompression = (int)Win32.BiCompression.BI_RGB;

			if(NativeMethods.GetDIBits(hdc, offscreenBitmap, 0, (uint)offscreenBitmapSize.Height, IntPtr.Zero,
				ref bitmapInfo, (uint)Win32.DIBCOLORS.DIB_RGB_COLORS) == 0)
				Win32.ThrowExceptionForLastWin32Error();

			int bitmapSizeImage = *(int*)(bitmapHeader + BitmapSizeImageOffset);
			if(bitmapSizeImage != bitmapInfo.bmiHeader_biSizeImage)
				throw new ApplicationException("Invalid image size.");

			//int bitmapInfoHeaderSize = bitmapInfoHeaderSize;
			//byte* bitmapInfoHeaderPtr = (byte*)&bitmapInfo.bmiHeader_biSize;
			//for(int i = 0; i < bitmapInfoHeaderSize; i++)
			//    bitmapHeader[i] = bitmapInfoHeaderPtr[i];
			
			if(NativeMethods.GetDIBits(hdc, offscreenBitmap, 0, (uint)offscreenBitmapSize.Height, new IntPtr(bitmapData),
				ref bitmapInfo, (uint)Win32.DIBCOLORS.DIB_RGB_COLORS) == 0)
				Win32.ThrowExceptionForLastWin32Error();

			MakeBitmapTrnasparent((uint*)bitmapData, bitmapSizeImage, transparentColor);
		}

		private unsafe void MakeBitmapTrnasparent(uint* bitmapBits, int bitmapBitsSize, uint transparentColor)
		{
			if(bitmapBits == null || bitmapBitsSize == 0)
				throw new ArgumentOutOfRangeException("bitmapBits");

			uint* startPtr = bitmapBits;
			uint* endPtr = bitmapBits + bitmapBitsSize / sizeof(uint);
			for(uint* ptr = startPtr; ptr < endPtr; ptr++)
			{
				if(*ptr == transparentColor)
					*ptr &= 0x00FFFFFF;	// completely transparent
				else
					*ptr |= 0xFF000000;	// completely opaque
			}
		}

		public void Register(string name)
		{
			_oshSource = new OSH.SourceClass();
			_oshSource.Create(name, 3, MaxBitmapSize.Width, MaxBitmapSize.Height);
		}

		public void Unregister()
		{
			if(_oshSource != null)
			{
				_oshSource.Close();
				Marshal.ReleaseComObject(_oshSource);
				_oshSource = null;
			}
		}


		#region IAdviseSink Members
		void IAdviseSink.OnClose()
		{
		}

		void IAdviseSink.OnDataChange(ref FORMATETC format, ref STGMEDIUM stgmedium)
		{
		}

		void IAdviseSink.OnRename(IMoniker moniker)
		{
		}

		void IAdviseSink.OnSave()
		{
		}

		void IAdviseSink.OnViewChange(int aspect, int index)
		{
			Osh.Log.Write(Configuration.Local.OshTraceSource, "IAdviseSink.OnViewChange()");
		}
		#endregion
	}
}
